package org.queueify;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;

import javax.servlet.http.HttpServletRequest;

import org.queueify.annotation.Param;
import org.queueify.annotation.QueueName;
import org.queueify.annotation.Retry;
import org.queueify.annotation.ServletPath;

import com.google.appengine.api.datastore.Transaction;
import com.google.appengine.api.taskqueue.QueueFactory;
import com.google.appengine.api.taskqueue.RetryOptions;
import com.google.appengine.api.taskqueue.TaskHandle;
import com.google.appengine.api.taskqueue.TaskOptions;
import com.google.appengine.api.taskqueue.TaskOptions.Method;

public class QueueifyFactory {
	
	final static private String CLASSNAME_PARAM = "-c";
	
	private interface ParamMap {
		String getParam(String key);
	}
	
	public TaskHandle queueTask(Transaction txn, QueueifyTask task) throws QueueifyException {
		ArrayList<QueueifyTask> list = new ArrayList<QueueifyTask>();
		list.add(task);
		List<TaskHandle> handles = queueTask(txn, list);
		return handles.get(0);
	}

	public List<TaskHandle> queueTask(Transaction txn, java.lang.Iterable<? extends QueueifyTask> tasks) throws QueueifyException {
		// we expect the entire iterable list to contain objects of the same type.
		// We are only going to examine the first one on the list to decide the queue name and servlet path.
		Iterator<? extends QueueifyTask> it = tasks.iterator();
		if ( it.hasNext() ) {
			QueueifyTask task = it.next();
			Class<?> klass = task.getClass();
			QueueName queueName = klass.getAnnotation(QueueName.class);
			if ( queueName == null ) {
				throw new QueueifyException("Missing @QueueName annotation in " + klass.getName());
			}
			ServletPath servletPath = klass.getAnnotation(ServletPath.class);
			if ( (servletPath == null) && (task instanceof QueueifyPushQueueTask) ) {
				throw new QueueifyException("Missing @ServletPath annotation in " + klass.getName());
			}
			if ( (servletPath != null) && !(task instanceof QueueifyPushQueueTask) ) {
				throw new QueueifyException("Unnecessary @ServletPath annotation in " + klass.getName());
			}

			ArrayList<TaskOptions> list = new ArrayList<TaskOptions>();
			TaskOptions t = createTaskOptions(klass, servletPath, task);
			list.add(t);
			while ( it.hasNext() ) {
				task = it.next();
				t = createTaskOptions(klass, servletPath, task);
				list.add(t);
			}

			// put the tasks on the queue
			return QueueFactory.getQueue(queueName.value()).add(txn, list);
		}
		throw new QueueifyException("task list is empty.");
	}

	private TaskOptions createTaskOptions(Class<?> klass, ServletPath servletPath, QueueifyTask task) throws QueueifyException {
		// start constructing the TaskOptions to submit to the queue.
		TaskOptions t = TaskOptions.Builder.withDefaults();
		if ( servletPath == null ) {
			// we are a PULL QUEUE task, mark the task as a PULL task.
			t.method(TaskOptions.Method.PULL);
			// check that we aren't using @Retry annotation
			Retry retry = klass.getAnnotation(Retry.class);
			if ( retry != null ) {
				throw new QueueifyException("Unnecessary @Retry annotation in " + klass.getName());
			}
		} else {
			// PUSH QUEUE task needs a url for the servlet that will handle it.
			t = t.url(servletPath.value()).method(Method.GET);			
			addRetryOptions(klass, t);
		}
		t = t.param(CLASSNAME_PARAM, klass.getName());
		
		// string-ify fields marked as @Param
		Field[] fields = klass.getDeclaredFields();
		for ( Field f : fields ) {
			if ( f.isAnnotationPresent(Param.class) ) {
				f.setAccessible(true);
				String val = FieldConverter.intoString(task, f);
				if ( val != null ) {
					t = t.param(f.getName(), val);
				}
			}
		}
		
		if ( task instanceof QueueifyPushQueueTask ) {
			long delay = ((QueueifyPushQueueTask)task).getCountdownMillis();
			if ( delay > 0 ) {
				t.countdownMillis(delay);
			}
		}
		
		return t;
	}

	private void addRetryOptions(Class<?> klass, TaskOptions t) {
		// where any retry options specified?
		Retry retry = klass.getAnnotation(Retry.class);
		if ( null != retry ) {
			RetryOptions options = RetryOptions.Builder.withDefaults();
			if ( retry.minBackoffSeconds() > 0 ) {
				options.minBackoffSeconds(retry.minBackoffSeconds());
			}
			if ( retry.maxBackoffSeconds() > 0 ) {
				options.maxBackoffSeconds(retry.maxBackoffSeconds());
			}
			if ( retry.maxDoublings() > 0 ) {
				options.maxDoublings(retry.maxDoublings());
			}
			if ( retry.taskAgeLimitSeconds() > 0 ) {
				options.taskAgeLimitSeconds(retry.taskAgeLimitSeconds());
			}
			if ( retry.taskRetryLimit() > 0 ) {
				options.taskRetryLimit(retry.taskRetryLimit());
			}
			t = t.retryOptions(options);
		} else {
			t = t.retryOptions(RetryOptions.Builder.withDefaults());
		}
	}

	public QueueifyPushQueueTask reconstructPushQueueTask(final HttpServletRequest req) throws QueueifyException {
		return (QueueifyPushQueueTask) reconstruct(new ParamMap() {
			@Override
			public String getParam(String key) {
				return req.getParameter(key);
			}
		});
	}
	
	public <T extends QueueifyPullQueueTask> T reconstructPullQueueTask(TaskHandle handle) throws QueueifyException {
		try {
			List<Entry<String, String>> paramList = handle.extractParams();
			final HashMap<String, String> params = new HashMap<String, String>();
			for ( Entry<String, String> e : paramList ) {
				params.put(e.getKey(), e.getValue());
			}
			T task = reconstruct(new ParamMap() {
				@Override
				public String getParam(String key) {
					return params.get(key);
				}
			});
			task.setTaskHandle(handle);
			return task;
		}
		catch (UnsupportedEncodingException e) {
			throw new QueueifyException(e);
		}
		catch (UnsupportedOperationException e) {
			throw new QueueifyException(e);
		}
	}
	
	private <T extends QueueifyTask> T reconstruct(ParamMap map) throws QueueifyException {
		String className = map.getParam(CLASSNAME_PARAM);
		if ( null == className ) {
			throw new QueueifyException("cannot determine classname of task");
		}
		try {
			@SuppressWarnings("unchecked")
			T task = (T) Class.forName(className).newInstance(); // expecting the class to have a no-args constructor
			Class<?> klass = task.getClass();
			Field[] fields = klass.getDeclaredFields();
			for ( Field f : fields ) {
				if ( f.isAnnotationPresent(Param.class) ) {
					f.setAccessible(true);
					String val = map.getParam(f.getName());
					FieldConverter.intoField(val, task, f);
				}
			}
			return task;
		}
		catch (InstantiationException e) { throw new QueueifyException(e); }
		catch (IllegalAccessException e) { throw new QueueifyException(e); }
		catch (ClassNotFoundException e) { throw new QueueifyException(e); }
	}
	
	// TODO check that the class matches the type indicated by the CLASSNAME_PARAM in the stored task. If not a match,
	// then perhaps modify its lease such that it is "put back" on the queue, and don't include it in the returned list.
	//  this will permit mixed types to be submitted to the same PULL queue.
	public <T extends QueueifyPullQueueTask> List<T> leaseTasks(Class<? extends QueueifyPullQueueTask> klass, long lease, java.util.concurrent.TimeUnit unit, long countLimit) throws QueueifyException {
		QueueName queueName = klass.getAnnotation(QueueName.class);
		if ( queueName == null ) {
			throw new QueueifyException("Missing @QueueName annotation in " + klass.getName());
		}
		List<TaskHandle> leased = QueueFactory.getQueue(queueName.value()).leaseTasks(lease, unit, countLimit);
		ArrayList<T> list = new ArrayList<T>();
		for ( TaskHandle handle : leased ) {
			list.add(this.<T>reconstructPullQueueTask(handle));
		}
		return list;
	}

	public <T extends QueueifyPullQueueTask> List<java.lang.Boolean> deleteTask(List<T> taskList) throws QueueifyException {
		if ( taskList.size() == 0 ) {
			return new ArrayList<java.lang.Boolean>(); 
		}
		Class<?> klass = taskList.get(0).getClass();
		QueueName queueName = klass.getAnnotation(QueueName.class);
		if ( queueName == null ) {
			throw new QueueifyException("Missing @QueueName annotation in " + klass.getName());
		}
		ArrayList<TaskHandle> taskHandles = new ArrayList<TaskHandle>();
		for ( T t : taskList ) {
			taskHandles.add(t.getTaskHandle());
		}
		return QueueFactory.getQueue(queueName.value()).deleteTask(taskHandles);
	}

	public <T extends QueueifyPullQueueTask> boolean deleteTask(T task) throws QueueifyException {
		Class<?> klass = task.getClass();
		QueueName queueName = klass.getAnnotation(QueueName.class);
		if ( queueName == null ) {
			throw new QueueifyException("Missing @QueueName annotation in " + klass.getName());
		}
		return QueueFactory.getQueue(queueName.value()).deleteTask(task.getTaskHandle());
	}

}
